package com.opennews.openplatform.familyexpenseapi.service

import com.opennews.openplatform.familyexpenseapi.entity.Tables.USER_ACTIVE_TOKEN
import com.opennews.openplatform.familyexpenseapi.entity.tables.records.UserActiveTokenRecord
import com.opennews.openplatform.familyexpenseapi.jooq.DSLContextWrapper
import com.opennews.openplatform.familyexpenseapi.security.SecurityActiveTokenManager
import com.opennews.openplatform.myspringbootcore.security.MyUserDetails
import com.opennews.openplatform.myspringbootcore.util.DateHelper
import org.jooq.DSLContext
import org.springframework.stereotype.Service

@Service
class UserActiveTokenService(
    private val dslContext: DSLContext,
    private val dslContextWrapper: DSLContextWrapper,
    private val securityActiveTokenManager: SecurityActiveTokenManager,
) {
    /**
     * Queries all devices which logged in with the provided username.
     *
     * @param username The username.
     * @return A list of maps containing:
     * - "id": String of the record ID.
     * - "deviceId": String of the device ID.
     * - "deviceModel": String of the device model.
     * - "enabled": Boolean indicating if the device is enabled.
     * - "lastUpdated": Timestamp of last update.
     */
    fun queryDeviceByUsername(username: String): List<Map<String, Any>> {
        return dslContext
            .select(
                USER_ACTIVE_TOKEN.ID.`as`("id"),
                USER_ACTIVE_TOKEN.DEVICE_ID.`as`("deviceId"),
                USER_ACTIVE_TOKEN.DEVICE_MODEL.`as`("deviceModel"),
                USER_ACTIVE_TOKEN.ENABLED.`as`("enabled"),
                USER_ACTIVE_TOKEN.LAST_UPDATED.`as`("lastUpdated"),
            )
            .from(USER_ACTIVE_TOKEN)
            .where(USER_ACTIVE_TOKEN.USERNAME.eq(username))
            .orderBy(USER_ACTIVE_TOKEN.LAST_UPDATED.desc())
            .fetchMaps()
    }

    /**
     * Checks if the refresh token is still active. If the data does not exist, it means the token has been revoked.
     * Refresh tokens do not expire, so they are not cached like access tokens.
     *
     * @param username The username.
     * @param refreshToken The refresh token.
     * @return True if the token is active, false otherwise.
     */
    fun isActiveRefreshToken(username: String, deviceId: String?, refreshToken: String): Boolean {
        var condition = USER_ACTIVE_TOKEN.USERNAME.eq(username)
            .and(USER_ACTIVE_TOKEN.REFRESH_TOKEN.eq(refreshToken))
            .and(USER_ACTIVE_TOKEN.ENABLED.isTrue())

        if (deviceId == null) {
            condition = condition.and(USER_ACTIVE_TOKEN.DEVICE_ID.isNull)
        } else {
            condition = condition.and(USER_ACTIVE_TOKEN.DEVICE_ID.eq(deviceId))
        }

        return dslContextWrapper.exists(USER_ACTIVE_TOKEN, condition)
    }

    /**
     * Updates or inserts the access token and refresh token by username and deviceId.
     * This can track the same user login via different agent.
     * If you want to revoke the token just need to delete this row.
     *
     * @param myUserDetails User details.
     * @param ipAddress Client request IP address.
     * @param deviceInfo Map containing user device basic info.
     * @param systemInfo Map containing user device system info.
     */
    fun updateTokens(
        myUserDetails: MyUserDetails,
        ipAddress: String,
        deviceInfo: Map<String, String>?,
        systemInfo: Map<String, String>?,
    ): Boolean {
        val query = dslContext.selectFrom(USER_ACTIVE_TOKEN)
            .where(
                USER_ACTIVE_TOKEN.USERNAME.eq(myUserDetails.username)
            ).let { baseQuery ->
                val deviceId = deviceInfo?.get("id")

                if (deviceId != null) {
                    baseQuery.and(USER_ACTIVE_TOKEN.DEVICE_ID.eq(deviceId))
                } else {
                    baseQuery.and(USER_ACTIVE_TOKEN.DEVICE_ID.isNull.or(USER_ACTIVE_TOKEN.DEVICE_ID.eq("")))
                }
            }

        var activeToken = query.fetchOneInto(UserActiveTokenRecord::class.java)

        // If enabled == false means login from this device is forbidden.
        if (activeToken?.enabled == false) {
            return false
        }

        if (activeToken == null) {
            activeToken = dslContext.newRecord(USER_ACTIVE_TOKEN)
            activeToken.username = myUserDetails.username
            activeToken.enabled = true
        }

        val dateGenerated = DateHelper.getCurrentLocalDateTime()
        val dateExpired = dateGenerated.plusSeconds(myUserDetails.expiresIn.toLong())

        activeToken.accountGroupId = myUserDetails.accountGroupId.asString()
        activeToken.ipAddress = ipAddress
        activeToken.deviceId = deviceInfo?.get("id")
        activeToken.deviceBrand = deviceInfo?.get("brand")
        activeToken.deviceModel = deviceInfo?.get("model")
        activeToken.deviceProductName = deviceInfo?.get("productName")
        activeToken.deviceSystemName = deviceInfo?.get("systemName")
        activeToken.deviceSystemVersion = deviceInfo?.get("systemVersion")
        activeToken.operatingSystem = systemInfo?.get("operatingSystem")
        activeToken.operatingSystemVersion = systemInfo?.get("operatingSystemVersion")
        activeToken.platformVersion = systemInfo?.get("platformVersion")
        activeToken.appVersion = systemInfo?.get("appVersion")
        activeToken.expiration = myUserDetails.expiresIn
        activeToken.accessToken = myUserDetails.accessToken
        activeToken.refreshToken = myUserDetails.refreshToken
        activeToken.dateGenerated = dateGenerated
        activeToken.dateExpired = dateExpired
        activeToken.store()

        // Stores the token in Redis with an expiration time if performance is critical.
        // Since refresh token does not expire then we should stick it in database then.
        // redisTemplate.opsForValue().set(myUserDetails.accessToken, myUserDetails.username, myUserDetails.expiresIn, TimeUnit.SECONDS);

        securityActiveTokenManager.reloadCachedUserActiveTokenRecords()

        return true
    }

    /**
     * Updates the enabled property for the user active token with provided id.
     *
     * @param id The ID of the token.
     * @param username The username associated with the token.
     * @param enabled Whether the token should be enabled or not.
     */
    fun updateEnabled(id: String, username: String, enabled: Boolean) {
        dslContext.update(USER_ACTIVE_TOKEN)
            .set(USER_ACTIVE_TOKEN.ENABLED, enabled)
            .where(
                USER_ACTIVE_TOKEN.ID.eq(id)
                    .and(USER_ACTIVE_TOKEN.USERNAME.eq(username))
            )
            .execute()

        securityActiveTokenManager.reloadCachedUserActiveTokenRecords()
    }

    /**
     * Deletes active data by access token.
     *
     * @param accessToken The access token string.
     */
    fun deleteByAccessToken(accessToken: String?) {
        if (!accessToken.isNullOrBlank()) {
            dslContext.deleteFrom(USER_ACTIVE_TOKEN)
                .where(USER_ACTIVE_TOKEN.ACCESS_TOKEN.eq(accessToken))
                .execute()

            // Removes from Redis
            // redisTemplate.delete(accessToken)

            securityActiveTokenManager.reloadCachedUserActiveTokenRecords()
        }
    }

    /**
     * Deletes active data by id and username.
     *
     * @param id The string of id.
     * @param username The string of username.
     */
    fun delete(id: String, username: String) {
        dslContext.deleteFrom(USER_ACTIVE_TOKEN)
            .where(
                USER_ACTIVE_TOKEN.ID.eq(id)
                    .and(USER_ACTIVE_TOKEN.USERNAME.eq(username))
            )
            .execute()

        securityActiveTokenManager.reloadCachedUserActiveTokenRecords()
    }

    /**
     * Deletes active data by username and account group id.
     *
     * @param username The string of the username.
     * @param accountGroupId The string of the account group id.
     */
    fun deleteByUserAndAccountGroup(username: String, accountGroupId: String) {
        dslContext.deleteFrom(USER_ACTIVE_TOKEN)
            .where(
                USER_ACTIVE_TOKEN.USERNAME.eq(username)
                    .and(USER_ACTIVE_TOKEN.ACCOUNT_GROUP_ID.eq(accountGroupId))
            )
            .execute()

        securityActiveTokenManager.reloadCachedUserActiveTokenRecords()
    }

    /**
     * Deletes active data by account group id.
     *
     * @param accountGroupId The string of the account group id.
     */
    fun deleteByAccountGroup(accountGroupId: String) {
        dslContext.deleteFrom(USER_ACTIVE_TOKEN)
            .where(USER_ACTIVE_TOKEN.ACCOUNT_GROUP_ID.eq(accountGroupId))
            .execute()

        securityActiveTokenManager.reloadCachedUserActiveTokenRecords()
    }
}